
local setmetatable, getmetatable, tostring, load, pairs, ipairs, next, assert, unpack, select
	= setmetatable, getmetatable, tostring, load, pairs, ipairs, next, assert, unpack, select
local strfind, strsub, strmatch, gmatch, strformat
	= string.find, string.sub, string.match, string.gmatch, string.format
local cowrap, yield
    = coroutine.wrap, coroutine.yield
local tconcat
	= table.concat
local floor
	= math.floor


module (...)


local function expectmatch_aux(sr, start, stop, ...)
	if not start then
		sr.lastmatch = nil
		return false
	else
		sr._pos = stop + 1
		if select('#', ...) > 1 then
			sr.lastmatch = {select(2, ...)}
			return select(2, ...)
		else
			sr.lastmatch = {...}
			return ...
		end
	end
end

local stringreader_meta = {
	__index = {
		expect = function(self, patt, exact)
			local pos = self._pos
			if exact then
				if strsub(self._source_text, pos, pos+#patt-1) == patt then
					self._pos = pos+#patt
					self.lastmatch = patt
					return patt
				else
					return false
				end
			end
			if (strsub(patt,1,1) == '^') then
				if (pos > 1) then
					return nil
				end
				patt = strsub(patt,2)
			end
			patt = '^(' .. patt .. ')'
			if (strsub(patt,-2) == '$)') and (strsub(patt,-3,-3) ~= '%') then
				patt = strsub(patt,1,-3) .. ')$'
			end
			return expectmatch_aux(self, strfind(self._source_text, patt, pos))
		end;
		endofinput = function(self)
			return self._pos > #self._source_text;
		end;
		skipwhitespace = function(self)
            self._pos = strmatch(self._source_text, '%s*()', self._pos)
		end;
		skipto = function(self, patt, exact)
			local new_pos = strfind(self._source_text, patt, self._pos, exact)
			if new_pos then
				self._pos = new_pos
				return true
			else
				return false
			end
		end;
		readchar = function(self)
			local pos = self._pos
			local c = strsub(self._source_text,pos,pos)
			self._pos = pos + 1
			return c
		end;
		peekchar = function(self)
			local pos = self._pos
			return strsub(self._source_text,pos,pos)
		end;
		pos = function(self, new_pos, relative)
			if new_pos then
				self._pos = relative and (self._pos+new_pos) or new_pos
				return self
			else
				return self._pos
			end
		end;
		getlines = function(self)
			local lines = self._lines
			if not lines then
				lines = {1}
				for linestart in gmatch(self._source_text, '\n()') do
					lines[#lines+1] = linestart
				end
				self._lines = lines
			end
			return lines
		end;
		pos2d = function(self, pos, relative)
			pos = pos and (relative and (self._pos+pos) or pos) or self._pos
			if (pos < 1) then  return nil;  end
			local lines = self:getlines()
			
			local low, high = 1, #lines
			while true do
				local line_num = floor((low + high) / 2)
				local line_start = lines[line_num]
				if (line_start > pos) then
					high = line_num - 1
				else
					local next_line_start = lines[line_num+1]
					if (next_line_start == nil) or (next_line_start > pos) then
						return line_num, pos - line_start + 1
					end
					low = line_num + 1
				end
			end
		end;
		source_text = function(self, new_value)
			if new_value then
				self._source_text = new_value
			else
				return self._source_text
			end
		end;
		rest = function(self)
			return strsub(self._source_text, self._pos)
		end;
	}
}

function reader(source_text)
	return setmetatable({_source_text=tostring(source_text), _pos=1}, stringreader_meta)
end

local sb_meta = {
	__tostring = function(self)  return tconcat(self);  end;
	__call = function(self,v)  self[#self+1] = tostring(v); return self;  end;
	__index = {
		add = function(self,v)
			self[#self+1] = tostring(v)
			return self
		end;
		addquoted = function(self, v)
			self[#self+1] = strformat('%q', tostring(v))
			return self
		end;
		compile = function(self, chunk_name)
			local localvars = self.localvars
			local params = self.params
			if not next(localvars) and not params then
                local chunkthread = cowrap(function()
                    for _,chunk in ipairs(self) do
                        yield(chunk)
                    end
                    yield(nil)
                end)
                return assert(load(chunkthread,chunk_name))
			end
			local locals, ilocals = {}, 0
			local chunkthread = cowrap(function()
				if next(localvars) then
					yield 'local '
					for k,v in pairs(localvars) do
						yield(k)
						if next(localvars,k) then
							yield ','
						end
						ilocals = ilocals+1
						locals[ilocals] = v
					end
					yield ' = ...; '
				end
				yield 'return function('
				if params then
					yield(params)
				end
				yield ') '
				for _,chunk in ipairs(self) do
					yield(chunk)
				end
				yield '\nend'
				yield(nil)
			end)
			local funcbuilder = assert(load(chunkthread,chunk_name))
			return funcbuilder(unpack(locals,1,ilocals))
		end;
		localvar = function(self, val)
			local lv = self.localvars
			local i = 1
			local lvname
			while true do
				lvname = '__lv_' .. i
				if not lv[lvname] then
					break
				end
				i = i + 1
			end
			lv[lvname] = val
			return lvname
		end;
	}
}

function builder()
	return setmetatable({localvars={}}, sb_meta)
end
